\documentclass[a4paper,10pt]{article}
\usepackage{graphicx}
\usepackage[utf8]{inputenc}
\usepackage[portuguese,brazil]{babel}
\usepackage[T1]{fontenc}
\usepackage{indentfirst}
\usepackage{url}
\usepackage{float}
\floatstyle{boxed}
\restylefloat{figure}
\begin{document}
\begin{titlepage}
\begin{minipage}{0.2\linewidth}
\includegraphics[]{./minerva.png}
\end{minipage}
\begin{minipage}{0.8\linewidth}
\textbf{Universidade Federal do Rio de Janeiro}\\
Instituto Alberto Luiz Coimbra de Pós-Graduação e Pesquisa de Engenharia\\
Programa de Engenharia de Sistemas e Computação\\
\rule{0.8\linewidth}{0.5mm}\\
Rio de Janeiro, RJ - Brasil
\end{minipage}
\begin{center}
\vspace{2cm}
\Large
Implementação do algoritmo SIFT para detecção de objetos em imagens. \\
\vspace{1cm}
\large
Rafael Oliveira Lopes \\
\vspace{0.5cm}
Roberto Buaiz Simão \\
\vspace{1.5cm}
Relatório gerado em \today
\normalsize
\end{center}
\vfill
\begin{flushright}
Disciplina: Processamento de Imagens 2011/1\\
Professor: Marroquim e Antônio\\
\end{flushright}
\vspace{2cm}
\end{titlepage}
\pagebreak
\tableofcontents
\pagebreak

\section{O problema proposto}

O problema proposto consiste em implementar o algoritmo SIFT (scale-invariant feature detector), desenvolvido por David Lowe em 1999. O SIFT é um algoritmo que encontra pontos fundamentais na imagem, chamados de \emph{keypoints}, atribuindo aos mesmos uma assinatura característica.

O SIFT pode ser utilizado para obter dados sobre uma imagem. Esses dados são comumente chamados de descritores. Tendo encontrado descritores de características nas quais temos interesse, podemos utilizá-los para comparar imagens, detectar imagens dentro de outras imagens ou mesmo guardá-los como uma assinatura da própria imagem.

Ao comparar imagens diferentes, podemos comparar os \emph{keypoints}, e, quando as assinaturas de um certo conjunto dos pontos possuírem valores muito próximos, podemos dizer que esses pontos representam o mesmo objeto retratado nas duas imagens.

O SIFT foi proposto com a intenção de resolver o problema de detectar imagens dentro de imagens quando elas foram sujeitas a rotações, translações e mudanças de escala. Desde sua proposta inicial, Lowe melhorou o algoritmo e realizou experimentos mais variados, possibilitando que o SIFT ficasse amplamente conhecido e fosse implementado em diversas linguagens e ambientes, bem como com o uso de técnicas distintas para melhorar o desempenho.

\section{Funcionamento do SIFT}

O algoritmo SIFT faz uso de uma estrutura conhecida como \emph{espaço de escala}, que consiste em, através da realização de borramentos gaussianos sucessivos e operações de redução de escala, obter uma espécie de pirâmide de gaussianas da imagem original.
Com essa pirâmide, o SIFT busca aproximar o valor do laplaciano da imagem em cada estágio da pirâmide. Lowe mostrou no artigo que, para isso, basta escolher um fator multiplicativo $k$ para o borramento $\sigma$ de cada gaussiana de forma a possibilitar um espaçamento mínimo nas escalas. Através da operação de diferença de imagens nas gaussianas de cada nível, é possível encontrar pontos extremos, possíveis futuros \emph{keypoints}. Basta comparar cada ponto de uma diferença de gaussianas com seus oito vizinhos na mesma imagem e com os nove vizinhos de cada diferença imediatamente anterior e posterior da mesma escala, totalizando 26 comparações.

De posse dos pontos de máximo e mínimo locais, o algoritmo calcula então orientações e magnitudes (ou seja, em coordenadas polares) para cada ponto próximo a um desses extremos segundo um critério de proximidade: os pontos que estiverem numa janela gaussiana de borramento $\frac{3}{2}\sigma$, onde $\sigma$ é o borramento local em cada gaussiana de cada escala. Em seguida, monta-se um histograma dos pontos nessa janela gaussiana cujos valores possíveis são intervalos de tamanho uniforme no comprimento do círculo trigonométrico e usando como pesos para a inserção no histograma as magnitudes desses pontos. Escolhe-se o valor máximo em cada histograma e os valores que estiverem em uma faixa de $80$ a $100\%$ do valor máximo, cada um virando um novo \emph{keypoint}. Aqui, Lowe sugere o uso de uma interpolação com o uso de uma parábola para obter um valor mais fiel para a orientação de cada \emph{keypoint}.

A partir da obtenção dessa orientação e magnitude representativa, o algoritmo monta então vetores que objetivam descrever a imagem através das orientações mais comuns ao redor deles e das magnitudes dessas orientações.

\section{Implementação e Particularidades}

Nesta seção abordaremos a implementação do algoritmo, detalhando alguns pontos que achamos que merecem destaque.

A implementação se deu utilizando a linguagem C++ com a biblioteca OpenCV. A biblioteca serviu apenas para facilitar os processos que não eram relevantes ao escopo do trabalho, como abertura e a gravação de imagens em formato \emph{jpeg}, algumas alterações estéticas (como por exemplo indicar com setas os \emph{keypoints} encontrados ou alterar uma imagem colorida para preto e branco).

Nossa implementação pressupõe os seguintes fatores sobre a entrada:

\begin{itemize}
\item a imagem de entrada deve ter dimensões entre $10 \times 10$ e $1500 \times 1500$ pixels;
\item pode-se utilizar imagens coloridas com três canais em 32 bits (como um arquivo \emph{jpeg} ou \emph{png} típico), mas o nosso trabalho realiza uma conversão para apenas um canal em 8 bits;
\item o nome do arquivo de entrada deve ser informado como único parâmetro para o arquivo executável (efeito que pode ser obtido simplesmente arrastando-se uma imagem para o arquivo executável);
\item se não for informado um nome de imagem de entrada, assumimos \emph{entrada.jpg}.
\end{itemize}

Na implementação, utilizamos o filtro gaussiano com valores em ponto-flutuante com uma janela truncada o suficiente (conforme Lowe descreve no artigo), em vez de uma aproximação inteira usando o Triângulo de Pascal. Também decidimos separar a janela do filtro em dois vetores e executar a passagem do filtro, primeiro aplicando-o às colunas e depois aplicando às linhas da imagem. Este método foi mencionado em sala de aula e nos garantiu uma melhoria significativa no tempo de execução. Para verificar o funcionamento do filtro gaussiano, realizamos uma comparação com a execução do filtro implementado pelo OpenCV e constatamos que os valores das janelas dos filtros dos dois diferenciavam-se apenas a partir da décima casa decimal.

Para obter uma orientação representativa dos \emph{keypoints}, o algoritmo gera um histograma, e a partir da orientação de maior valor no histograma e seus dois vizinhos, gera uma parábola, determinando seu valor mínimo (ou máximo) e determinando como orientação deste \emph{keypoint} a orientação a que o ponto de máximo pertence. Neste passo, o algoritmo pode gerar novos \emph{keypoints}, se os valores no histograma diferirem muito significativamente. Decidimos determinar a orientação simplesmente com o valor de máximo no histograma, pois julgamos que essa era uma aproximação pertinente ao escopo do trabalho, já que em muitos casos a orientação que tem maior valor no histograma possui o valor mínimo ou máximo na parábola gerada.

Temos então o último passo que consiste em gerar um descritor para cada \emph{keypoint}. Aqui, consideramos que de posse da localização de cada \emph{keypoint}, das suas orientações pertinentes (escolhemos apenas uma no passo anterior), da escala em que ele foi encontrado e do valor de $\sigma$ utilizado para suavização na sua escala, gerar os descritores seria uma etapa seguinte trivial.

\section{Dificuldades encontradas}

Logo de início, foi necessário implementar a operação de convolução $2D$. Para testá-la, implementamos alguns filtros vistos na aula: Sobel, Roberts, laplaciano. Em seguida, demos continuidade à implementação do próprio SIFT, que se revelou de início traumática.

A principal dificuldade encontrada durante o desenvolvimento do SIFT ocorreu com a escolha do valor de $\sigma$ para o borramento em cada escala. A determinação do valor do fator multiplicativo $k$ não se encontrava bem explicada no artigo de Lowe, apesar do valor estar definido. Ao comparar com outras implementações previamente desenvolvidas do SIFT, o valor de $k$ se encontrava diferente em cada implementação. Após uma análise cuidadosa, e com o auxílio do professor, definimos o valor do fator multiplicativo $k$ como $\sqrt{2}$, o que está em consonância com o primeiro artigo de Lowe sobre o SIFT.

Por causa da dificuldade na determinação do fator de borramento, achamos que a nossa implementação do filtro gaussiano não estava correta. Ao curso das duas últimas semanas, descobrimos que ela difere muito pouco do filtro gaussiano do próprio OpenCV. Esse fato atrasou o desenvolvimento e nos obrigou a reduzir o escopo do trabalho.

Pretendíamos estender a implementação para de fato procurar correspondências de imagens umas nas outras, mas não houve tempo hábil. Nos concentramos assim em terminar uma implementação funcional do algoritmo, determinar os \emph{keypoints} e desenhá-los na imagem.

\section{Conclusões}

Nossa implementação do SIFT não contém muitos dos refinamentos presentes nas descrições do Lowe e muito provavelmente presentes na implementação que ele disponibiliza online. Apesar disso, apresenta um resultado satisfatório, por localizar pontos-chave para o reconhecimento de objetos de imagens.

Para chegar a tal conclusão, executamos o programa desenvolvido por Lowe e que se encontra como exemplo no site dele. Como é um programa de código fechado, não pudemos determinar os parâmetros usados pelo programa, e por isso assumimos que haverá uma diferença natural nos resultados.

Uma dificuldade encontrada neste momento foi com a manipulação de imagens. O programa de Lowe opera com imagens do tipo PGM, formato que utiliza apenas tons de cinza. Para comparar os resultados das implementações, foi necessário realizar uma conversão do formato JPG para o formato PGM para aplicar a implementação de Lowe, assim como para executar a nossa implementação bastava o formato JPG, sem conversão.

A conversão de formatos gera uma alteração em algumas características de imagem, que por serem imperceptíveis a nosso olho destreinado, podem ocasionar a ocorrência de mais diferenças ao comparar os resultados das duas implementações. Apresentados esses dois problemas, podemos dizer que os resultados, em suas devidas proporções, foram bastante satisfatórios.

Também pudemos verificar que a nossa implementação detecta muito mais \emph{keypoints} locais em comparação com a implementação de Lowe. Acreditamos que isso se deve à falta de refino na passagem dos histogramas de orientação para a orientação propriamente dita. Em contrapartida, pudemos observar \emph{keypoints} detectados pela nossa implementação que parecem ter relevância e não estão presentes com a mesma frequência nos resultados da implementação de Lowe.

Não realizamos também um teste exaustivo dos parâmetros: o fator de borramento, a quantidade de níveis em cada escala da pirâmide, os pontos de corte de contraste e curvatura. Nosso trabalho foi completado muito próximo ao prazo de entrega.

\section{Agradecimentos}

Por fim, agradecemos a orientação recebida do professor Marroquim. Com a implementação do algoritmo funcional, podemos olhar para trás e descobrir o quanto aprendemos no decurso desse trabalho e em tão pouco tempo. Tivemos a oportunidade de combinar um trabalho de implementação com o estudo da teoria -- já que ambos se deram simultaneamente -- em uma área de estudos que não imaginávamos tão ampla.

\end{document}

